<?php
class ZReader {
    /**
     * Current connection host name.
     * This variable use to store the host name that Reader try to connect to retrieve data from it.
     * This variable's value was read from config file and init in constructor.
     * @var string
     */
    private $host;

    /**
     * @var string
     */
    private $port;

    /**
     * @var
     */
    public $errno;

    /**
     * @var
     */
    public $errstr;

    /**
     * @var int
     */
    private $timeout;

    /**
     * Current sock open handler
     * @var resource
     */
    private $sockfd = null;

    /**
     * @var int
     */
    protected $offset;
    /**
     * @var int
     */
    protected $limit;
    /**
     * @var string (dd-mm-yyyy)
     */
    protected $begin;
    /**
     * @var string (dd-mm-yyyy)
     */
    protected $end;
    /**
     * @var int
     */
    protected $timeZone;
    /**
     * @var string
     */
    protected $query;
    /**
     * @var string
     */
    protected $languages;
    /**
     * @var string
     */
    protected $types;
    /**
     * @var string
     */
    protected $session;
    /**
     * @var string (comma separated)
     */
    protected $excludedIds;
    /**
     * @var string (comma separated)
     */
    protected $excludedThreadIds;

    /**
     * @param String $begin (dd-mm-yyyy)
     * @param String $end (dd-mm-yyyy)
     * @param Int $timeZone
     * @param String $query
     * @param Int[] $langs
     * @param Int[] $types
     */
    public function __construct($begin, $end, $timeZone, $query, $langs = array(), $types = array()) {
        $this->host             = ZConfig::$zHost;
        $this->port             = ZConfig::$zPort;
        $this->timeout          = ZConfig::$zTimeout;
        $this->streamTimeout    = ZConfig::$zStreamTimeout;
        $this->session          = '';//pack('NN', 0, 0);

        $this->begin            = date('YmdH',strtotime($begin) - $timeZone * 3600);
        $this->end              = date('YmdH', strtotime($end) - $timeZone * 3600 + 82800);
        $this->query            = $query;
        $this->languages        = implode(',', (array)$langs);
        $this->types            = implode(',', (array)$types);
        //Default TimeZone
        $this->timeZone         = $timeZone;
    }

    /**
     *
     */
    function __destruct() {
        $this->close();
    }

    /**
     * @return resource
     * @throws Exception
     */
    private function connect(){
        //Check for current sockfd is opened, if not just create new connection, otherwise close it first.
        $this->close();

        //Create new connection
        $this->sockfd = @fsockopen($this->host, $this->port, $this->errno, $this->errstr, $this->timeout);

        if (!$this->sockfd) {
            throw new Exception(sprintf(
                'Unable to connect to server at hostname %s:%d! Error received: (%d) %s',
                $this->host,
                $this->port,
                $this->errno,
                $this->errstr
            ), 100);
        }

        return $this->sockfd;
    }

    /**
     * @param $com
     * @return resource
     */
    protected function send($com){
        $f = $this->connect();

        if ($f){
            stream_set_timeout($f, $this->streamTimeout);

            $size       = strlen($com);
            $written    = 0;
            $com        = chr(0).substr(pack('V', $size), 0, 3).$com;
            $size       += 4;

            do {
                $rv = fwrite($f, substr($com, $written));
                if ($rv === false || $rv === 0) {
                    return false;
                }
                $written += $rv;
            }
            while($written < $size);
        }
        return $f;
    }

    /**
     * @param int $numBytes
     * @return float
     * @throws Exception
     */
    protected function readNum($numBytes = 4) {
        if ($this->sockfd) {
            try {
                //Check if data available for reading
                //In default mode, data reading will block the thread until it receive enough data needed,
                //so, if client close their browser while the thread is blocked, it still using server resource.
                //This loop below check for is there any data available for reading, and also check for client status is aborted or not,
                //if there is at least one byte to read, it jump out of the while,
                //if client was disconnected, it also jump out and break.
                do {
                    $read   = array($this->fd());
                    $write  = NULL;
                    $except = NULL;
                    $n = stream_select($read, $write, $except, 0, 500);
                    //This script below use to detect is browser still opening
                    //Because of this null character may let json (javascript eval) wrong, so we just comment it now
                    /*echo chr(0);
                    ob_flush();
                    flush();*/
                }
                while($n === 0 && !connection_aborted());

                //Client is disconnected
                if (connection_aborted()) die;

                //When data available, read it
                $buffer = fread($this->sockfd, $numBytes);

                if ($buffer) {
                    if ($numBytes <= 1) {
                        $tmp = unpack('C', $buffer);
                        return (int)$tmp[1];
                    }
                    else if ($numBytes <= 2) {
                        $tmp = unpack('S', $buffer);
                        return (int)$tmp[1];
                    }
                    else if ($numBytes <= 4) {
                        $hex = unpack('H*', strrev($buffer));
                        return hexdec($hex[1]);
                    }
                    else if ($numBytes <= 8) {
                        $hex = unpack('H*', strrev($buffer));
                        return hexdec($hex[1]);
                    }
                }
                return -1;
            }
            catch(Exception $e) {
                throw new Exception('Can not read number from the connection!');
            }
        }
        return null;
    }

    /**
     * @param $numBytes
     * @return null|string
     * @throws Exception
     */
    protected function readStr($numBytes) {
        if ($this->sockfd) {
            if ($numBytes == 0) {
                return '';
            }
            else if ($numBytes < 0) {
                throw new Exception('The size of data to read must be greater than zero!');
            }
            //Read data
            try {
                //Check if data available for reading
                //In default mode, data reading will block the thread until it receive enough data needed,
                //so, if client close their browser while the thread is blocked, it still using server resource.
                //This loop below check for is there any data available for reading, and also check for client status is aborted or not,
                //if there is at least one byte to read, it jump out of the while,
                //if client was disconnected, it also jump out and break.
                do {
                    $read   = array($this->fd());
                    $write  = NULL;
                    $except = NULL;
                    $n = stream_select($read, $write, $except, 0, 500);
                    //This script below use to detect is browser still opening
                    //Because of this null character may let json (javascript eval) wrong, so we just comment it now
                    /*echo chr(0);
                    ob_flush();
                    flush();*/
                }
                while($n === 0 && !connection_aborted());

                //Client is disconnected
                if (connection_aborted()) die;

                //The buffer memory
                $data = '';

                //Because of normal php limit memory to 8KB maximum, so we need to loop through when numBytes needed is larger than memory limit.
                $read = 1;
                while ($numBytes > 0 && $read > 0 && !$this->eof()) {
                    $buffer = fread($this->sockfd, $numBytes);

                    $data .= $buffer;

                    $read = strlen($buffer);
                    $numBytes -= $read;
                }
                return $data;
            }
            catch(Exception $e) {
                throw new Exception('Can not read string data from the connection!');
            }
        }
        return null;
    }

    /**
     * Check current connection is ended
     * @return bool
     */
    protected function eof() {
        return feof($this->sockfd);
    }

    /**
     * Close current connection and release all it's memory
     */
    protected function close() {
        if ($this->sockfd) {
            fclose($this->sockfd);
            $this->sockfd = null;
        }
    }

    /**
     * Read the status
     * @return int
     */
    protected function readStatus() {
        $status = -1;
        while($status < 0 && !$this->eof()) {
            $status = $this->readNum(1);
        }

        if ($status == 0) {
            //Read session
            $num        = $this->readNum(2); //Number of block 8 byte
            $session    = '';
            for($i = 0; $i < $num; $i++) {
                $session .= $this->readStr(8);
            }
            $this->session = $session;
        }

        return $status;
    }

    /**
     * Read the error messages response from server, it usually come follow status code provide by readStatus() function
     * @return int
     */
    protected function readMsg() {
        return $this->readStr(1024);
    }

    /**
     * Get the sock file descriptor
     * @return resource
     */
    protected function fd() {
        return $this->sockfd;
    }

    /**
     * Start reading data progress
     * @param null $callback
     * @param int $offset = 0
     * @param int $limit = 0
     * @throws Exception
     * @return int|ZCollection (Number of returned items or Collection)
     */
    protected function readData($callback = null, $offset = 0, $limit = 0) {
        $this->offset   = $offset;
        $this->limit    = $limit;

        $command        = $this->command();

        //Send command to server
        $this->send($command);

        //Status checking
        if (($status = $this->readStatus()) != 0)
        {
            throw new Exception(
                sprintf('Error [%d] %s.',
                    $status,
                    $status > 0 ? $this->readMsg() : 'Server having problem now and it is trying to fix itself automatically! Please try again after at least 2 minutes or contact the administrator!'
                )
                , 102
            );
        }

        //Parse data
        $state = 0;
        $collection = $this->parse($callback, $state);

        //Not successful loading from server
        switch($state) {
            case 0:
                throw new Exception('Your query has no result! Try again!', 103);
                break;
            case 1:
                //file_put_contents($cache, serialize($collections));
                break;
            case -1:
                throw new Exception('Data received without ending flag! It\'s mean there is a lot of data was missed because of the connection with server.', 101);
                break;
            default:
                throw new Exception('Not handled parse data return');
        }

        //Close connection
        $this->close();

        return $collection;
    }

    protected function command() {
        return null;
    }

    protected function parse($callback, &$state) {
        return $state = -2;
    }

    /**
     * Set current working session
     * @param string $ses
     */
    public function setSession($ses) {
        $this->session = $ses;
    }

    /**
     * Get current working session
     * @return string
     */
    public function getSession() {
        return $this->session;
    }

    /**
     * Set the list of id that will be excluded from result
     * @param int[] $ids
     */
    public function setExcludedIds($ids) {
        $this->excludedIds = implode(',', $ids);
    }

    /**
     * Get the list of id that will be excluded from result
     * @return int[]
     */
    public function getExcludedIds() {
        return explode(',', $this->excludedIds);
    }

    /**
     * Set the list of thread id that will be excluded from result
     * @param int[] $ids
     */
    public function setExcludedThreadIds($ids) {
        $this->excludedThreadIds = implode(',', $ids);
    }

    /**
     * Get the list of thread id that will be excluded from result
     * @return int[]
     */
    public function getExcludedThreadIds() {
        return explode(',', $this->excludedThreadIds);
    }
}

?>
